﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Data.Metadata.Edm;
using System.Data.Services.Design;
using System.IO;
using System.Xml;
using System.CodeDom;
using System.Reflection;
using System.ComponentModel.DataAnnotations;

namespace Niagara.Core
{
  /// <summary>
  /// 
  /// </summary>
  public class ValidationDataServiceProxyGenerator
  {
    #region Fields
    IAttributeMetadataProvider _metadataProvider;
    TextWriter _outputWriter = null;
    XmlReader _edmMetadata;
    string _rootNamespace;
    LanguageOption _language = LanguageOption.GenerateCSharpCode;
    DataServiceCodeVersion _dataServiceVersion = DataServiceCodeVersion.V1;
    bool _useDataServiceCollection = false;
    string _currentTypeName = null;
    bool _generateValidation = false;
    #endregion // Fields

    #region Construction
    /// <summary>
    /// Initializes a new instance of the <see cref="ValidationDataServiceProxyGenerator"/> class.
    /// </summary>
    /// <param name="edmMetadata">The edm metadata.</param>
    /// <param name="outputWriter">The output writer.</param>
    /// <param name="rootNamespace">The root namespace.</param>
    /// <param name="metadataProvider">The metadata provider.</param>
    public ValidationDataServiceProxyGenerator(XmlReader edmMetadata, TextWriter outputWriter, string rootNamespace, IAttributeMetadataProvider metadataProvider)
    {
      _metadataProvider = metadataProvider;
      _outputWriter = outputWriter;
      _rootNamespace = rootNamespace;
      _edmMetadata = edmMetadata;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="ValidationDataServiceProxyGenerator"/> class.
    /// </summary>
    /// <param name="edmMetadata">The edm metadata.</param>
    /// <param name="outputFilename">The output filename.</param>
    /// <param name="rootNamespace">The root namespace.</param>
    /// <param name="metadataProvider">The metadata provider.</param>
    public ValidationDataServiceProxyGenerator(XmlReader edmMetadata, string outputFilename, string rootNamespace, IAttributeMetadataProvider metadataProvider)
      : this(edmMetadata, new StreamWriter(outputFilename), rootNamespace, metadataProvider)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="ValidationDataServiceProxyGenerator"/> class.
    /// </summary>
    /// <param name="edmMetadata">The edm metadata.</param>
    /// <param name="outputFilename">The output filename.</param>
    /// <param name="rootNamespace">The root namespace.</param>
    public ValidationDataServiceProxyGenerator(XmlReader edmMetadata, string outputFilename, string rootNamespace)
      : this(edmMetadata, outputFilename, rootNamespace, null)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="ValidationDataServiceProxyGenerator"/> class.
    /// </summary>
    /// <param name="edmMetadata">The edm metadata.</param>
    /// <param name="outputWriter">The output writer.</param>
    /// <param name="rootNamespace">The root namespace.</param>
    public ValidationDataServiceProxyGenerator(XmlReader edmMetadata, TextWriter outputWriter, string rootNamespace)
      : this(edmMetadata, outputWriter, rootNamespace, null)
    {
    }
    #endregion // Construction

    #region Methods
    /// <summary>
    /// Generates the code.
    /// </summary>
    /// <returns></returns>
    public IList<EdmSchemaError> GenerateCode()
    {
      var gen = new EntityClassGenerator(_language);

      gen.OnTypeGenerated += new EventHandler<TypeGeneratedEventArgs>(gen_OnTypeGenerated);
      gen.OnPropertyGenerated += new EventHandler<PropertyGeneratedEventArgs>(gen_OnPropertyGenerated);

      gen.UseDataServiceCollection = _useDataServiceCollection;
      gen.Version = _dataServiceVersion;


      var result = gen.GenerateCode(_edmMetadata, _outputWriter, _rootNamespace);
      _outputWriter.Flush();
      return result;
    }
    #endregion

    #region Eventhandlers
    void gen_OnPropertyGenerated(object sender, PropertyGeneratedEventArgs e)
    {
      // If can supply validation and has a current type, try and provide it
      if (_metadataProvider != null &&
          _currentTypeName != null &&
          e.PropertySource.BuiltInTypeKind == BuiltInTypeKind.EdmProperty)
      {
        // Find the name of the property
        var metadata = e.PropertySource.MetadataProperties;
        var propertyName = e.PropertySource.MetadataProperties.Where(m => m.Name == "Name").FirstOrDefault();
        if (propertyName != null)
        {
          // Retrieve the Attributes
          var attributes = _metadataProvider.GetPropertyAttributes(_currentTypeName, propertyName.Value as string);
          foreach (var attribute in attributes)
          {
            // Add it to the codegen
            var codeAttribute = CreateAttributeDeclaration(attribute);
            e.AdditionalAttributes.Add(codeAttribute);
          }
        }

        // Add call to Validation
        e.AdditionalSetStatements.Insert(0, new CodeExpressionStatement(
          new CodeMethodInvokeExpression(
            new CodeMethodReferenceExpression(
              new CodeThisReferenceExpression(),
              "ValidateProperty"),
            new CodePrimitiveExpression(propertyName.Value),
            new CodeVariableReferenceExpression("value"))));
      }

    }


    void gen_OnTypeGenerated(object sender, TypeGeneratedEventArgs e)
    {
      if (this._metadataProvider != null && e.TypeSource.BuiltInTypeKind == BuiltInTypeKind.EntityType)
      {
        var nameMetadata = e.TypeSource.MetadataProperties.Where(m => m.Name == "Name").FirstOrDefault();
        var namespaceMetadata = e.TypeSource.MetadataProperties.Where(m => m.Name == "NamespaceName").FirstOrDefault();
        if (nameMetadata != null && namespaceMetadata != null)
        {
          _currentTypeName = string.Concat(namespaceMetadata.Value, ".", nameMetadata.Value);
        }
        else
        {
          _currentTypeName = null;
        }

        e.AdditionalMembers.Add(CreateValidationMethod());
      }
    }
    #endregion // Event Handlers

    #region Properties
    /// <summary>
    /// Gets or sets the language.
    /// </summary>
    /// <value>The language.</value>
    public LanguageOption Language
    {
      get { return _language; }
      set { _language = value; }
    }

    /// <summary>
    /// Gets or sets the data service version.
    /// </summary>
    /// <value>The data service version.</value>
    public DataServiceCodeVersion DataServiceVersion
    {
      get { return _dataServiceVersion; }
      set { _dataServiceVersion = value; }
    }

    /// <summary>
    /// Gets or sets a value indicating whether [use data service collection].
    /// </summary>
    /// <value>
    /// 	<c>true</c> if [use data service collection]; otherwise, <c>false</c>.
    /// </value>
    public bool UseDataServiceCollection
    {
      get { return _useDataServiceCollection; }
      set { _useDataServiceCollection = value; }
    }

    /// <summary>
    /// Gets or sets a value indicating whether [generate validation].
    /// </summary>
    /// <value><c>true</c> if [generate validation]; otherwise, <c>false</c>.</value>
    public bool GenerateValidation
    {
      get { return _generateValidation; }
      set { _generateValidation = value; }
    }
    #endregion // Properties

    #region CodeGen Helpers
    // Thanks to @KristofA for this code
    static CodeAttributeDeclaration CreateAttributeDeclaration(object attribute)
    {
      string attributeTypeName = attribute.GetType().FullName;
      if (attributeTypeName.EndsWith("Attribute"))
      {
        attributeTypeName = attributeTypeName.Substring(0, attributeTypeName.Length - "Attribute".Length);
      }

      if (attribute.GetType().IsAssignableFrom(typeof(ValidationAttribute)))
      {
        throw new ArgumentException(string.Concat("The attribute is not an ValidationAttribute derived class: ", attributeTypeName));
      }

      var attributeType = attribute.GetType();
      var properties = attributeType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty);
      var ctors = attributeType.GetConstructors(BindingFlags.Public | BindingFlags.Instance);

      if (ctors.Count() == 0)
      {
        throw new ArgumentException("The attribute type does not contain any public constructors");
      }

      object defaultValuesAttrib = null;
      List<CodeAttributeArgument> ctorArguments = new List<CodeAttributeArgument>();
      ConstructorInfo chosenCtor = null;

      // Find the right Constructor
      // HACKY SOLUTION
      // Looking for constructor arguments that are named the same as "Get" properties
      foreach (var ctor in ctors.OrderBy(c => c.GetParameters().Length))
      {
        var parameters = ctor.GetParameters();

        if (parameters.Count() == 0)
        {
          defaultValuesAttrib = Activator.CreateInstance(attributeType);
          chosenCtor = ctor;
        }
        else
        {
          // Clear the argument to ensure that only arguments for this ctor are used
          ctorArguments.Clear();
          object[] ctorParams = new object[parameters.Count()];
          int counter = 0;
          foreach (var parameter in parameters)
          {
            var property = properties.FirstOrDefault(p => p.Name.ToLower() == parameter.Name.ToLower());
            if (property != null)
            {
              object value = property.GetValue(attribute, new object[] { });
              if (value == null)
              {
                //throw new ArgumentException(
                //  string.Format("Could not determine attribute parameter for {0} attribute.  Parameter = {1}", 
                //    attribute.GetType().Name,
                //    parameter.Name));
                break;
              }

              ctorParams[counter++] = value;
              Type valueType = value.GetType();
              if (valueType.IsPrimitive || valueType == typeof(string))
              {
                ctorArguments.Add(new CodeAttributeArgument(new CodePrimitiveExpression(
                          value
                      )));
              }
              else if (valueType.IsEnum)
              {
                ctorArguments.Add(new CodeAttributeArgument(new CodeFieldReferenceExpression(
                  new CodeTypeReferenceExpression(value.GetType()),
                    Enum.GetName(value.GetType(), value))));
              }
              else
              {
                throw new ArgumentException("Cannot use non-primitive or non-enumeration attribute parameters.  Send issue to Niagara.codeplex.com to get feature added.");
              }

              if (counter >= parameters.Count())
              {
                chosenCtor = ctor;
                defaultValuesAttrib = Activator.CreateInstance(attributeType, ctorParams);
              }
            }
          }
        }
      }

      if (defaultValuesAttrib == null)
      {
        throw new ArgumentException(string.Concat("Could not find an appropriate constructor for attribute: ", attributeTypeName));
      }

      //add DataMember attrib to the member property
      CodeAttributeDeclaration dataMemberAttribute = null;
      if (ctorArguments.Count == 0)
      {
        dataMemberAttribute = new CodeAttributeDeclaration(
            new CodeTypeReference(
            attributeTypeName
        ));
      }
      else
      {
        dataMemberAttribute = new CodeAttributeDeclaration(
            new CodeTypeReference(
            attributeTypeName
        ), ctorArguments.ToArray());
      }

      var propertyFilter = new Func<PropertyInfo, bool>(t => t.PropertyType.IsPrimitive || t.PropertyType == typeof(string) || t.PropertyType.IsEnum);
      foreach (PropertyInfo attribArgument in attribute.GetType().GetProperties().Where(propertyFilter))
      {
        object attribArgumentValue = attribArgument.GetValue(attribute, null);
        object defaultValue = attribArgument.GetValue(defaultValuesAttrib, null);

        if (attribArgumentValue != null
            && !attribArgumentValue.Equals(defaultValue))
        {
          if (attribArgument.PropertyType.IsEnum)
          {
            dataMemberAttribute.Arguments.Add(
              new CodeAttributeArgument(
                attribArgument.Name,
                new CodeFieldReferenceExpression(
                  new CodeTypeReferenceExpression(attribArgumentValue.GetType()),
                    Enum.GetName(attribArgumentValue.GetType(), attribArgumentValue)
                )
              )
            );
          }
          else
          {
            dataMemberAttribute.Arguments.Add(
              new CodeAttributeArgument(
                attribArgument.Name,
                new CodePrimitiveExpression(
                  attribArgumentValue
                )
              )
            );
          }
        }
      }

      return dataMemberAttribute;
    }

    static object CreateDefaultInstance(object attribute)
    {
      var attributeType = attribute.GetType();
      var properties = attributeType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty);
      var ctors = attributeType.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
      if (ctors.Count() == 0)
      {
        throw new ArgumentException("The attribute type does not contain any public constructors");
      }

      foreach (var ctor in ctors)
      {
        var parameters = ctor.GetParameters();

        if (parameters.Count() == 0)
        {
          return Activator.CreateInstance(attributeType);
        }
        else
        {
          object[] ctorParams = new object[parameters.Count()];
          int counter = 0;
          foreach (var parameter in parameters)
          {
            var property = properties.FirstOrDefault(p => p.Name.ToLower() == parameter.Name.ToLower());
            object value = property.GetValue(attribute, new object[] { });
            ctorParams[counter++] = value;
            if (counter >= parameters.Count())
            {
              return Activator.CreateInstance(attributeType, ctorParams);
            }
          }
        }
      }

      return null;
    }

    static CodeMemberMethod CreateValidationMethod()
    {
      var valMethod = new CodeMemberMethod()
      {
        Name = "ValidateProperty",
        Attributes = MemberAttributes.Family
      };

      valMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(string), "propertyName"));
      valMethod.Parameters.Add(new CodeParameterDeclarationExpression(typeof(object), "value"));

      var createContext = new CodeVariableDeclarationStatement(
        "System.ComponentModel.DataAnnotations.ValidationContext",
        "ctx",
        new CodeObjectCreateExpression("System.ComponentModel.DataAnnotations.ValidationContext",
          new CodeThisReferenceExpression(),
          new CodePrimitiveExpression(null),
          new CodePrimitiveExpression(null))
          );


      var setMemberName = new CodeAssignStatement(
        new CodePropertyReferenceExpression(
          new CodeVariableReferenceExpression("ctx"),
          "MemberName"),
        new CodeVariableReferenceExpression("propertyName"));

      var callValidation = new CodeMethodInvokeExpression(
        new CodeMethodReferenceExpression(
          new CodeTypeReferenceExpression("System.ComponentModel.DataAnnotations.Validator"),
          "ValidateProperty"),
       new CodeVariableReferenceExpression("value"),
       new CodeVariableReferenceExpression("ctx"));

      valMethod.Statements.Add(createContext);
      valMethod.Statements.Add(setMemberName);
      valMethod.Statements.Add(callValidation);


      return valMethod;
    }
    #endregion
  }
}
